Run Your Own Instant Messaging Service on FreeBSD

What if you could host your own instant messaging service for you and your friends, to communicate privately and securely, away from the prying eyes of big tech? Turns out you can, and it’s actually quite easy to do.

Run Your Own Instant Messaging Service on FreeBSD

When I grew up instant messaging wasn’t really a thing, mainly because few people had internet connectivity at home. While the ability to communicate with others on the computer is as old as the 1960s, and the Internet Relay Chat dates back to the 1980s, instant messaging as we know it today only really took off in the late 90s. Back at the time, ICQ, AIM and Yahoo! Messenger were the most prominent names in that domain. I remember most of my friends and family being on ICQ, with only few (more technical) people running mIRC to hang out on Freenoe, Undernet, euIRC and the QuakeNet.

I’d say the late-1990s was the time when the instant messaging goldrush began, and while it dimmed down throughout the early 2000s and was in parts overtaken by SMS and – in the corporate world – by BBM, it really took off with smartphones becoming a thing in 2009. That’s also when WhatsApp launched their service, with others like Kik and Facebook Messenger following suit shortly after.

The IM market today looks vastly different from the one twenty years ago. I don’t know anyone using ICQ, even though the service has been re-launched (over and over again) and is still available to this day. And while AIM, MSN and other messengers have shared a similar fate, there’s one example that has successfully stood the test of time: XMPP, or Jabber.

XMPP isn’t a service, as for example AIM or the MSN Messeger have been, but an open protocol. It specifies the way individual implementations of XMPP are supposed to communicate with each other, similar to how SMTP specifies the communication between individual mail services. As with mail software, there are various proprietary implementations that use(d) XMPP for their service, like for example Google Talk, and there are a large number of open source implementations, allowing everyone to set up and run their own XMPP service. And with XMPP being federated, it allows individually run services to communicate with each other, making it possible for one user on service A to reach out to another user on service B – similarly to how an @hotmail.com user could write e-mails to an @gmail.com user, even though they were both using different services.

XMPP these days supports a wide variety of extensions to the original protocol, called XEPs, allowing it to implement and make use of modern features like video calling or end-to-end encryption, while still remaining backward compatible (to certain degrees).

With that said, let’s have a look at how we could set up our own instant messaging service that uses E2EE (end-to-end encryption) and could totally replace WhatsApp, Telegram or Signal for our family, friends and ourselves!

Server

I’m going to use a Vultr VPS instance with FreeBSD 13.1, as it offers a solid basis for this setup as well as an up to date version of ejabberd – the XMPP implementation that we’re going to use. While I would have preferred to use OpenBSD, there’s no package of the XMPP server available there.

After launching the instance and SSHing into it, we’re first going to update the base system:

root@msg:~ # freebsd-update fetch install

We’re also taking care to update all installed packages:

root@msg:~ # pkg update && pkg upgrade

Let’s also reboot the system to apply kernel updates:

root@msg:~ # reboot

When I set up the VM, Vultr only had the FreeBSD 12 image available, hence we can perform an upgrade to 13.1. This is only necessary if the current FreeBSD version is older than the one you’re intending to use (13.1 in this case):

root@msg:~ # freebsd-update -r 13.1-RELEASE upgrade
root@msg:~ # freebsd-update install
root@msg:~ # reboot
root@msg:~ # freebsd-update install
root@msg:~ # pkg-static upgrade -f pkg
root@msg:~ # pkg bootstrap -f
root@msg:~ # pkg update && pkg upgrade
root@msg:~ # freebsd-update install

Next, let’s install a couple of handy tools:

root@msg:~ # pkg install tmux neovim mosh rsync

We are now able to reconnect using mosh instead of SSH, and use tmux to launch multiple shells on a single connection. Let’s continue by installing and configuring ejabberd:

root@msg:~ # pkg install ejabberd

We will have to set up the DNS first. I’ll be using the domain msg.example.com for our instant messaging service. Replace it with your actual domain. Also, if you’re looking for further details on the ejabberd configuration, please refer to the official documentation.

msg.example.com.		2400	IN	A	44.78.114.101
conference.msg.example.com.		2400	IN	A	44.78.114.101
proxy.msg.example.com.		2400	IN	A	44.78.114.101
pubsub.msg.example.com.		2400	IN	A	44.78.114.101
upload.msg.example.com.		2400	IN	A	44.78.114.101
stun.msg.example.com.		2400	IN	A	44.78.114.101
_stun._udp.msg.example.com.		2400	IN	SRV	0	0	3478	stun.msg.example.com.
_stun._tcp.msg.example.com.		2400	IN	SRV	0	0	3478	stun.msg.example.com.
_stuns._tcp.msg.example.com.		2400	IN	SRV	0	0	5349	stun.msg.example.com.
turn.msg.example.com.		2400	IN	A	44.78.114.101
_turn._udp.msg.example.com.		2400	IN	SRV	0	0	3478	turn.msg.example.com.
_turn._tcp.msg.example.com.		2400	IN	SRV	0	0	3478	turn.msg.example.com.
_turns._tcp.msg.example.com.		2400	IN	SRV	0	0	5349	turn.msg.example.com.

The A records are pointing to our Vultr instance’s IPv4 address. If you have an IPv6 address also add the AAAA records in a similar fashion.

Some registrars won’t allow you to properly input the SRV entries due to bugs in their UIs. If that’s the case you can skip these entries for now, as those are only really necessary for voice/video calling, which we won’t dive into here. You can find more info on these topics here.

Next we’re going to take a look at the configuration of ejabberd. You can find the yaml file under /usr/local/etc/ejabberd/ejabberd.yml (including a .example config in the same folder). I’m assuming that you’ll be using an ejabberd version >= 22.05 here. By default ejabberd already comes with a solid and sane configuration, however, we need to adjust a few values to make them fit our setup. First, change the hosts value to contain the domain you’ll be using the Jabber server under:

hosts:
  - msg.example.com

Next, configure the database and auth method to use Erlang’s own mnesia database, so that we won’t have to deal with a dedicated database. Unless you’re looking to serve dozens of users, mnesia will work just fine:

default_db: mnesia
new_sql_schema: true

host_config:
  msg.example.com:
    auth_method: mnesia

Under listen: we now have to make sure that the individual listeners are available from outside. With the IPv4 setup here you can set the ip: value to 0.0.0.0 for all listeners but ejabberd_http as well as mod_mqtt. For these two, set the ip: value to 127.0.0.1 in order to only have them listen on the local interface.

Additionally, make sure to set tls: true for all listeners that listen on 0.0.0.0.

Next, let’s add an admin user that we name root:

acl:
  admin:
    user: root
  local:
    user_regexp: ""
  loopback:
    ip:
      - 127.0.0.0/8
      - ::1/128

The access_rules should already contain rules for admin users.

Last but not least, we’re going to configure mod_mam as well as mod_push for now. For mod_mam we set the following configuration:

  mod_mam:
    db_type: mnesia

For mod_push we do the following, as we do not want message content to be sent through push notifications. If you prefer not pushing the sender name either, disable include_sender:

  mod_push:
    include_body: "New message!"
    include_sender: true

We’re basically done with the groundwork. Obviously there are plenty of other things to adjust and tune – see below – however, with these changes we should get ejabberd up and running for once.

Let’s enable ejabberd through the /etc/rc.conf by appending the following line:

ejabberd_enable="YES"

We also need to configure /usr/local/etc/relayd.conf to forward requests on port 80 to 5280, which is required for issuing LetsEncrypt certificates:

ipv4="44.78.114.101"
#ipv6=""

table <ejabberd_http> { 127.0.0.1 }

relay www4 {
        listen on $ipv4 port http

        forward to <ejabberd_http> port 5280
}

#relay www6 {
#        listen on $ipv6 port http
#
#        forward to <ejabberd_http> port 5280
#}

We are now able to (re)start relayd and launch ejabberd with the following commands:

root@msg:~ # service relayd restart
root@msg:~ # service ejabberd start

To see what’s going on, we can use the following command:

root@msg:~ # tail -f /var/log/ejabberd/ejabberd.log

Now let’s add the root user:

root@msg:~ # ejabberdctl register root msg.example.com 'my_password_here'

With this user, you should now be able to login on https://msg.example.com:5443/admin/ and use the web interface to create additional users and check the status of ejabberd. You can/should restrict access to the admin interface.

The ejabberd should now also be listening on msg.example.com:5222. After you’ve created yourself a regular user through the admin interface you can try connecting to it using an XMPP client, by entering <user>@msg.example.com as username and the password you’ve assigned to the user.

Before we dive into the client config though, I suggest adding the following crontab entry:

root@msg:~ # crontab -l
0       *       *       *       *       find /var/spool/ejabberd/upload -type f -cmin +60 -exec rm -rf {} \;

This crontab command checks the ejabberd’s upload folder – that’s where all files that are being transferred from one client to the other go – for files that are older than 60 minutes and deletes them. This is a safety/privacy enhancement that I would recommend doing, in order to not leave message attachments (e.g. photos or videos that were sent from one client to the other) on the server. XMPP clients usually download and cache attachments locally, meaning that as soon as a message was picked up from the server, the client will likely have the attachment cached locally, not requiring the file to be on the server anymore. The downside is of course that, if your client won’t pick up the message within 60 minutes, you won’t see the attachment anymore, only the message itself. Feel free to fine-tune the rule to make it work for you. At the end of the day, all attachments that are uploaded onto the XMPP will be encrypted anyway, meaning that even if your server gets compromised, the data might be of little use to the attacker.

Clients

There are a large number of clients to pick from and I won’t be able to cover every client in existence here. However, here’s a short list of my favorites:

  • Profanity: TUI client for *n.x, macOS, Windows, and Android (via Termux)
  • Dino: GUI client for *n.x
  • Gajim: GUI client for *n.x, macOS, and Windows
  • Conversations: GUI client for Android (no GSF/GCM/FCM needed)
  • Beagle IM: GUI client for macOS
  • Siskin IM: GUI client for iOS (has Push Notifications)

I have tested all of these clients with ejabberd and can confirm that messaging works extremely well. Each of these clients supports OMEMO encryption, which is a must for this sort of setup. In addition, the Siskin IM iOS client even supports push notifications, which however are sent through a third party server – which is why we adjusted the mod_push configuration to not send message content.

Noteworthy clients that I haven’t tested/used in a long time are:

  • Monal: macOS and iOS client
  • Pidgin: GUI client for *n.x, Windows and macOS

Configuration of any of these clients should be pretty straightforward. Usually simply logging in with user@msg.example.com and the password is sufficient for the client to know what to do.

Integrations

With XMPP being an open protocol with plenty client libraries around, there are a large number of integrations – like bridges and bots – available. matterbridge is a well-known example in that regard.

In case you’re using the Pushover service, you might find value in one of my own projects: pushover-to-xmpp, a lightweight bridge that forwards Pushover notifications to XMPP users.

Important ToDos

Obviously this is a super basic setup that should help you get going without having to invest too much time right away. This setup can be used to play around with XMPP, invite a few friends and see whether this might work as an alternative solution to the privacy-invasive instant messengers you might otherwise be using.

In order to turn this into a reliable and secure messaging platform however, there are some more things that need to be done:

  • Disable SSH access for root, set different port
  • Set up log monitoring, fail2ban
  • pkg install lynis, run lynis audit system -Q and follow hints
  • Restrict access to the admin console
  • Limit S2S connectivity to only servers you want to intentionally peer with (access_rules.s2s)
  • Set up a firewall, configure anti-spamming/-brute-forcing rules
  • Fine-tune ejabberd configuration, disable not needed features, set up an actual database backend if higher load is expected – Vultr offers managed Postgres
  • Set up Tor connectivity for increased privacy
  • Add redundancy, in case downtimes are intolerable
  • Dive into security(7) and the handbook

As implementing these things require an understanding of how the service will be used, it makes sense to start lightweight and continuously iterate/extend the setup over time.

Have fun messaging!


Enjoyed this? Support me via Monero, Bitcoin, Lightning, or Ethereum!  More info.